Professor: Marcos Cesar Gritti
Email: cesargritti@gmail.com
Antes de começar:
!pip install -r ../requirements.txtNeste módulo vamos aprender sobre:
No dia a dia de trabalho, um Cientista de Dados se depara com diferentes tipos fontes de dados. Nem sempre, em sua equipe, haverá um Engenheiro de Dados disponível para te ajudar a coletar dados de interesse em um formato fácil de integração com seu ambiente de desenvolvimento Python (ou qualquer outro ambiente de desenvolvimento científico), no nosso caso, o Jupyter Notebook. Portanto, é fundamental que você domine os principais formatos e/ou fontes existentes no mercado, para que não dependa de um terceiro para uma rápida prototipação/experimentação.
As principais fontes de dados, encontradas por um profissional da área, são:
Em empresas que seguem a filosofia Data Driven haverá, usualmente, uma pedaço de Software chamado de Camada de Ingestão de Dados. Esta camada, desenvolvida por Engenheiros de Dados, tem por objetivo centralizar diversas fontes de informação bruta (arquivos csv, json, parquet, imagens, audios, etc ...) em um único repositório (ou Buckets). Este repositório centralizado recebe o nome de Data Lake, e é o ponto de partida para processos de ETL (Extract, Transform and Load), e também a forma mais fácil de um Cientista de Dados se servir de dados.
A consulta à base de dados SQL/NoSQL está fora do escopo deste módulo, contudo, com o domínio da linguagem Python para o processamento dos principais tipos de arquivo citados acima, . Tampouco trabalharemos, neste módulo, com processamento de imagens/audio.
A existência de arquivos csv em Data Lakes não é predominante, pois, apesar de ser um arquivo fácil de se manipular, não é o mais eficiente (redução de espaço em disco e otimização de tempo de leitura). Entretanto, é o tipo de arquivo mais encontrado quando a informação ainda não está disponível no Data Lake (exportação de planilhas Excel, base de dados do IBGE, entre outras).
O arquivo csv é o melhor amigo do Pandas. Para carregar um arquivo em memória, utilizamos a função read_csv
dataset = pd.read_csv("caminho/do/arquivo.csv")
import pandas as pd
dados_csv = pd.read_csv("dados_brutos.csv")
dados_csv.head()
| uf | tipo | cod_localidade | feat_1 | feat_2 | feat_3 | feat_4 | loc_x | loc_y | mercado_mais_proximo | farmacia_mais_proxima | escola_mais_proxima | num_penit_4km | num_penit_500m | idade_imovel | area | preco | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | RJ | Tipo 1 | Localidade 4 | 0.620648 | 9.967806 | 4.990882 | 25.124844 | 0.081382 | 0.727021 | 3603.941384 | 2002.686030 | 1124.043113 | 0.0 | 0.0 | NaN | 123.0 | 1348017 |
| 1 | SP | Tipo 2 | Localidade 4 | 0.817642 | 12.629695 | 5.466835 | 23.444343 | 0.367980 | 0.145812 | 2185.209139 | 683.811862 | 2462.825432 | 0.0 | 0.0 | 13.0 | 143.0 | 926601 |
| 2 | RJ | Tipo 2 | Localidade 3 | 0.793080 | 11.292156 | 4.201919 | 28.230731 | 0.332654 | 0.432904 | 1025.698339 | 957.451552 | 1049.112117 | 0.0 | 0.0 | 12.0 | 150.0 | 1627474 |
| 3 | SC | Tipo 1 | Localidade 4 | 0.792435 | 11.563047 | 5.459777 | 22.414837 | 0.159663 | 0.884596 | NaN | 3723.067390 | 1296.121182 | 0.0 | 0.0 | 8.0 | 160.0 | 1201041 |
| 4 | RN | Tipo 1 | Localidade 3 | 0.711696 | 11.655785 | 4.891314 | 25.451251 | 0.156154 | 0.836320 | 3925.306331 | 705.807343 | 4178.062758 | 0.0 | 0.0 | 12.0 | 134.0 | 1444848 |
É o formato mais utilizado por Engenheiros de Software, devido à sua compatibilidade com as tecnologias de desenvolvimento de APIs da atualidade. Consequentemente, a quantidade de arquivos json em Data Lakes é volumosa.
No Pandas, importa-se um arquivo json utilizando o comando read_json
dados_json = pd.read_json("dados_brutos.json")
dados_json.head()
| uf | tipo | cod_localidade | feat_1 | feat_2 | feat_3 | feat_4 | loc_x | loc_y | mercado_mais_proximo | farmacia_mais_proxima | escola_mais_proxima | num_penit_4km | num_penit_500m | idade_imovel | area | preco | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 700 | ES | Tipo 1 | None | 0.638824 | 10.158127 | 4.874347 | 26.151255 | 0.632733 | 0.481356 | 341.680530 | 3456.862812 | 2557.124996 | 0.0 | 0.0 | 37.0 | NaN | 1329074 |
| 701 | SP | Tipo 2 | Localidade 4 | 0.794100 | 11.467263 | 4.889458 | 25.737262 | 0.290362 | 0.649488 | 3121.658324 | 2711.257761 | 2635.042549 | 0.0 | 0.0 | 4.0 | 151.0 | 980660 |
| 702 | MS | Tipo 1 | Localidade 4 | 0.745027 | 11.088365 | 4.644014 | 26.165747 | 0.117298 | 0.131615 | 1808.463617 | 1178.930223 | 1231.387072 | 0.0 | NaN | 15.0 | 170.0 | 1044861 |
| 703 | PR | Tipo 1 | Localidade 1 | 0.773947 | 12.182951 | 5.778339 | 21.948647 | 0.521053 | 0.021927 | 4189.517081 | 6402.599591 | 1738.502238 | 0.0 | 0.0 | 8.0 | 84.0 | 1347838 |
| 704 | RO | Tipo 2 | Localidade 1 | 0.686853 | 10.321383 | 4.589251 | 27.254870 | 0.985792 | 0.744716 | 385.176751 | 1630.761705 | 3446.457453 | 0.0 | 0.0 | 20.0 | 168.0 | 751177 |
É um formato de armazenamento colunar, disponível em todos os projetos do ecossistema Hadoop. Em suma, um arquivo parquet permite armazenar e consultar o arquivo de forma eficiênte, o que justifica seu emprego na construção de Data Lakes.
A API do Pandas é intuitiva! Para carregar um arquivo parquet, utilizamos o método pd.read_parquet
dados_parquet = pd.read_parquet("dados_brutos.parquet")
dados_parquet.head()
| uf | tipo | cod_localidade | feat_1 | feat_2 | feat_3 | feat_4 | loc_x | loc_y | mercado_mais_proximo | farmacia_mais_proxima | escola_mais_proxima | num_penit_4km | num_penit_500m | idade_imovel | area | preco | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | DF | Tipo 1 | Localidade 1 | 0.865418 | 12.469136 | 4.428843 | 28.599545 | 0.039081 | 0.967402 | 6424.017248 | 2312.613264 | 922.096367 | 0.0 | 0.0 | 9.0 | 60.0 | 1283960 |
| 1 | SC | Tipo 1 | Localidade 2 | 0.794821 | 11.643225 | 5.745631 | 20.793361 | 0.163092 | 0.150923 | 1390.553671 | 7062.080907 | 6311.945099 | 0.0 | 0.0 | 6.0 | 135.0 | 691992 |
| 2 | MG | Tipo 2 | Localidade 1 | 0.765382 | 11.246786 | 4.435433 | 27.697589 | 0.519452 | 0.083601 | NaN | 5795.794123 | 1406.914318 | 0.0 | 0.0 | 17.0 | 162.0 | 1042605 |
| 3 | SP | Tipo 2 | Localidade 1 | 0.742807 | 10.807940 | 4.510181 | 28.549110 | 0.094447 | 0.229071 | 2811.748941 | 5112.857979 | 1954.494335 | 1.0 | 0.0 | 3.0 | 105.0 | 1132298 |
| 4 | DF | Tipo 1 | Localidade 4 | 0.708509 | 10.182098 | 5.581803 | 22.486957 | 0.398567 | 0.594843 | 3362.149025 | 1685.887551 | 1600.735664 | 0.0 | 0.0 | 5.0 | 137.0 | 1133084 |
Os dados da aula de hoje foram divididos em três arquivos, os quais carregamos nas células anteriores. Pesquise na documentação do Pandas como unir as linhas dos dataframes dados_csv, dados_json e dados_parquet, e um novo dataframe nominado dados.
https://pandas.pydata.org/docs/reference/index.html#api
Dica: Concatenar é a palavra chave de pesquisa
# Substitua a igualdade abaixo por uma que empilhe as linhas dos três conjuntos de
# dados que carregamos anteriormente em apenas um conjunto de dados denominado `dados`
#dados = dados_csv.copy()
dados = pd.concat([dados_csv,dados_json,dados_parquet]).reset_index(drop=True)
dados
| uf | tipo | cod_localidade | feat_1 | feat_2 | feat_3 | feat_4 | loc_x | loc_y | mercado_mais_proximo | farmacia_mais_proxima | escola_mais_proxima | num_penit_4km | num_penit_500m | idade_imovel | area | preco | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | RJ | Tipo 1 | Localidade 4 | 0.620648 | 9.967806 | 4.990882 | 25.124844 | 0.081382 | 0.727021 | 3603.941384 | 2002.686030 | 1124.043113 | 0.0 | 0.0 | NaN | 123.0 | 1348017 |
| 1 | SP | Tipo 2 | Localidade 4 | 0.817642 | 12.629695 | 5.466835 | 23.444343 | 0.367980 | 0.145812 | 2185.209139 | 683.811862 | 2462.825432 | 0.0 | 0.0 | 13.0 | 143.0 | 926601 |
| 2 | RJ | Tipo 2 | Localidade 3 | 0.793080 | 11.292156 | 4.201919 | 28.230731 | 0.332654 | 0.432904 | 1025.698339 | 957.451552 | 1049.112117 | 0.0 | 0.0 | 12.0 | 150.0 | 1627474 |
| 3 | SC | Tipo 1 | Localidade 4 | 0.792435 | 11.563047 | 5.459777 | 22.414837 | 0.159663 | 0.884596 | NaN | 3723.067390 | 1296.121182 | 0.0 | 0.0 | 8.0 | 160.0 | 1201041 |
| 4 | RN | Tipo 1 | Localidade 3 | 0.711696 | 11.655785 | 4.891314 | 25.451251 | 0.156154 | 0.836320 | 3925.306331 | 705.807343 | 4178.062758 | 0.0 | 0.0 | 12.0 | 134.0 | 1444848 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2022 | SC | Tipo 2 | Localidade 4 | 0.923722 | 13.940390 | 5.222921 | 24.202279 | 0.047978 | 0.106754 | 1835.793637 | 1085.880075 | 1389.476784 | 0.0 | 0.0 | 3.0 | 160.0 | 889177 |
| 2023 | PR | Tipo 2 | Localidade 4 | 0.621180 | 9.638953 | 4.872834 | 26.311889 | 0.146383 | 0.696719 | 3241.890776 | 1942.810024 | 1713.583735 | 0.0 | 0.0 | 6.0 | 88.0 | 863004 |
| 2024 | ES | Tipo 1 | Localidade 3 | 0.702689 | 10.906394 | 5.002910 | 24.681539 | 0.998298 | 0.747620 | 3207.183221 | 1425.618869 | 1571.548396 | 0.0 | 0.0 | 40.0 | 97.0 | 1322275 |
| 2025 | SC | Tipo 1 | Tipo 3 | 0.840306 | 12.612464 | 5.191198 | 23.653668 | 0.565233 | 0.801478 | 2574.736799 | 1223.924147 | 150.881637 | 0.0 | 0.0 | 43.0 | 128.0 | 1170550 |
| 2026 | CE | Tipo 2 | Localidade 1 | 0.780135 | 11.537751 | 4.873889 | 26.015937 | 0.835390 | 0.302851 | 4390.423348 | 2626.528938 | 1230.964520 | 0.0 | 0.0 | 5.0 | 119.0 | 791071 |
2027 rows × 17 columns
Agora que carregamos os dados no notebook, precisamos explorá-los para encontrar eventuais inconsistências. No dia a dia de trabalho de um Cientista de Dados, é muito comum encontrar:
Dentre as etapas do processo de Mineração de Dados, a limpeza do conjunto de dados é a que despende maior tempo, e que tem papel chave quanto ao sucesso do projeto. Por quê? Como veremos adiante, alguns algoritmos de Aprendizado de Máquina são gulosos, ou seja, encontrarão uma resposta até mesmo para os ruídos presentes no seu conjunto de treinamento (conceito de Overfitting).
Vamos começar identificando que variáveis existem no conjunto, e seus respectivos tipos, utilizando os comandos:
dtypes: para verificar o tipo de cada coluna;sample(N): para coletar uma pequena amostra que pode nos ajudar a sanar dúvidas sobre os tipos;dados.dtypes
uf object tipo object cod_localidade object feat_1 float64 feat_2 float64 feat_3 float64 feat_4 float64 loc_x float64 loc_y float64 mercado_mais_proximo float64 farmacia_mais_proxima float64 escola_mais_proxima float64 num_penit_4km float64 num_penit_500m float64 idade_imovel float64 area float64 preco int64 dtype: object
dados.sample(5).T
| 977 | 787 | 690 | 823 | 1598 | |
|---|---|---|---|---|---|
| uf | SP | None | RJ | GO | SC |
| tipo | Tipo 1 | Tipo 2 | Tipo 1 | Tipo 2 | Tipo 2 |
| cod_localidade | Localidade 2 | Localidade 4 | Localidade 4 | Localidade 4 | Localidade 3 |
| feat_1 | 0.896537 | 0.82117 | 0.816033 | NaN | 0.870661 |
| feat_2 | 13.695373 | 12.409319 | 13.070487 | 10.546326 | 12.524437 |
| feat_3 | 4.89505 | 4.742783 | 5.733456 | NaN | 5.115338 |
| feat_4 | 25.926119 | 25.964902 | 21.333684 | 25.021204 | 23.90915 |
| loc_x | 0.31839 | 0.877227 | 0.322636 | 0.00939 | 0.205602 |
| loc_y | 0.629544 | 0.288078 | 0.408328 | 0.041152 | 0.045514 |
| mercado_mais_proximo | 3107.098813 | 3422.122721 | 1798.915576 | NaN | 4229.536893 |
| farmacia_mais_proxima | 2404.877557 | 4566.669067 | 3010.546472 | 1748.382604 | 4693.206369 |
| escola_mais_proxima | 2006.047028 | 1030.689936 | 1406.330673 | 2113.389868 | 1704.841762 |
| num_penit_4km | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| num_penit_500m | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
| idade_imovel | 11.0 | 17.0 | 14.0 | 25.0 | 28.0 |
| area | 109.0 | 127.0 | 86.0 | 78.0 | 158.0 |
| preco | 842624 | 959912 | 1153233 | 655796 | 1205894 |
Nosso conjunto de dados representa uma base imobiliária, e é formado pelas seguintes colunas:
| Nome | Descrição | Tipo |
|---|---|---|
| uf | O estado onde o imóvel está localizado | object (string) |
| tipo | O tipo do imóvel | object (string) |
| cod_localidade | Código qualitativo da localidade do imóvel (Consultoria) | object (string) |
| feat_1 | Feature 1 (Consultoria) | float64 |
| feat_2 | Feature 2 (Consultoria) | float64 |
| feat_3 | Feature 3 (Consultoria) | float64 |
| feat_4 | Feature 4 (Consultoria) | float64 |
| loc_x | Coordenada x do imóvel em um mapa local de referência (Consultoria) | float64 |
| loc_y | Coordenada y do imóvel em um mapa local de referência (Consultoria) | float64 |
| mercado_mais_proximo | Distância do mercado mais próximo, em metros | float64 |
| farmacia_mais_proxima | Distância da farmácia mais próxima, em metros | float64 |
| escola_mais_proxima | Distância da escola mais próxima, em metros | float64 |
| num_penit_4km | Número de penitenciárias em um raio de 4km do imóvel | int64 |
| num_penit_500m | Número de penitenciárias em um raio de 500m do imóvel | int64 |
| idade_imovel | Idade do imóvel | int64 |
| area | Área do imóvel, em $m^2$ | float64 |
| preco | O preço do imóvel, em R$ | int64 |
int64, no Python, representa o conjunto dos números naturais $\mathbb{N}$float64, no Python, representa o conjunto dos números reais $\mathbb{R}$object, no Python, pode representar uma string ou uma estrutura de dados composta (list, dict, classes customizadas, entre outras)As variáveis cod_localidade, feat_1, feat_2, feat_3, feat_4, loc_x e loc_y foram elaboradas por um time de consultoria externa especializada em avaliação imobiliária. Sabemos que os códigos de qualidade contidos na coluna cod_localidade são utilizados para segmentar subregiões em níveis de qualidade, contudo, não sabemos se as categorias podem ser interpretadas como variáveis ordinais. Quanto às variáveis numéricas intervalares (feat_1 à feat_4), sabe-se apenas que foram construídas com base em laudos históricos de avaliação de imóveis próximos, e com base em indicadores macro-econômicos.
Não existe uma receita de bolo exata para tratamento de dados, uma vez que a natureza e as regras de negócio variam muito de problema à problema. Para este caso de estudo, vamos começar verificando o conteúdo das variáveis de tipo object (assumindo como premissa de que são categoricas). Como já sabemos que estes campos estão armazenando valores do tipo string, uma boa pergunta inicial seria: Quais os possíveis valores destes campo?
Para responder a esta pergunta, podemos utilizar a função unique:
# Valores únicos de UF (incluíndo valores nulos, i.e., NaN)
dados.uf.unique()
array(['RJ', 'SP', 'SC', 'RN', 'CE', 'MG', 'ES', 'RO', 'DF', 'MS', 'PR',
'GO', 'AC', 'BA', 'MT', 'AL', 'RS', 'PB', 'TO', 'AM', 'AP', 'SE',
'RR', 'PI', 'MA', 'PE', 'PA', nan, None], dtype=object)
# Valores únicos de tipo
dados.tipo.unique()
array(['Tipo 1', 'Tipo 2', 'TIPO 1', 'tipo 2', nan, 'TIPO 2',
'LOCALIDADE 6', 'Localidade 9', 'Localidade 8', 'tipo 1',
'Localidade 6', 'Tipo 3', 'Tipo 4', None, 'Localidade 10',
'Localidade 5', 'TIPO 4', 'tipo 3', 'Localidade 7'], dtype=object)
# Valores únicos de localidade
dados.cod_localidade.unique()
array(['Localidade 4', 'Localidade 3', 'localidade 4', 'Localidade 2',
'Localidade 9', 'Localidade 1', 'Localidade 7', 'localidade 3',
'LOCALIDADE 2', 'localidade 1', 'LOCALIDADE 1', nan,
'Localidade 8', 'Tipo 3', 'localidade 2', 'LOCALIDADE 3',
'localidade 9', 'LOCALIDADE 4', 'Tipo 2', 'Localidade 5',
'Localidade 6', None, 'localidade 5', 'Tipo 4', 'Localidade 10',
'tipo 4', 'localidade 7'], dtype=object)
dados.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| feat_1 | 2022.0 | 0.751808 | 0.103431 | 0.222897 | 0.684783 | 0.754352 | 8.201856e-01 | 1.277357e+00 |
| feat_2 | 2022.0 | 11.304705 | 1.654269 | 3.067246 | 10.239090 | 11.362082 | 1.238432e+01 | 1.870217e+01 |
| feat_3 | 2016.0 | 5.004588 | 0.514403 | 3.214858 | 4.655714 | 4.999201 | 5.347276e+00 | 6.638993e+00 |
| feat_4 | 2023.0 | 24.978204 | 2.627059 | 15.850045 | 23.158389 | 25.002898 | 2.680118e+01 | 3.349520e+01 |
| loc_x | 2020.0 | 0.505169 | 0.288890 | 0.000530 | 0.250194 | 0.508069 | 7.473581e-01 | 9.997262e-01 |
| loc_y | 2023.0 | 0.501737 | 0.286487 | 0.000855 | 0.254863 | 0.510660 | 7.463950e-01 | 9.995796e-01 |
| mercado_mais_proximo | 1953.0 | 3086.351775 | 1714.499709 | 37.932408 | 1794.086282 | 2816.375170 | 4.090446e+03 | 9.733443e+03 |
| farmacia_mais_proxima | 1966.0 | 2675.509079 | 1538.196595 | 49.678746 | 1476.145321 | 2397.733125 | 3.622657e+03 | 8.063493e+03 |
| escola_mais_proxima | 1964.0 | 2429.899456 | 1318.803468 | 63.839485 | 1432.287866 | 2240.663181 | 3.273253e+03 | 7.336996e+03 |
| num_penit_4km | 2017.0 | 0.302925 | 0.468191 | 0.000000 | 0.000000 | 0.000000 | 1.000000e+00 | 2.000000e+00 |
| num_penit_500m | 2025.0 | 0.007901 | 0.088559 | 0.000000 | 0.000000 | 0.000000 | 0.000000e+00 | 1.000000e+00 |
| idade_imovel | 2022.0 | 15.410979 | 12.595984 | 0.000000 | 6.000000 | 11.000000 | 2.100000e+01 | 5.000000e+01 |
| area | 1968.0 | 124.730691 | 40.153210 | 51.000000 | 94.000000 | 120.000000 | 1.540000e+02 | 2.460000e+02 |
| preco | 2027.0 | 989262.435619 | 290317.635521 | 257922.000000 | 797689.000000 | 964698.000000 | 1.163877e+06 | 2.140795e+06 |
O que é quantil/percentil/quartil?
Quantils são pontos estabelecidos em intervalos regulares em uma lista ordenada que informa o percentual de dados abaixo de um limiar em uma amostra.
Exemplo de cálculo de quantil:
Dada uma lista de valores desordenados $[5, 3, 1, 10, 4]$ o quantil nos diz o número índice da lista no qual $x$% da população (elementos da lista) são menores do que o valor apontado pelo índice. Por exemplo, se ordenarmos a lista de forma crescente, obtemos $[1, 3, 4, 5, 10]$.
Para calcular o quantil 0.5 (equivalente a mediana, e também ao segundo quartil), basta encontrar o índice central da lista. Nesta caso, a lista contém 5 elementos, sendo o índice 3 seu elemento central. Logo, nosso $q_{0.5} = 4$, significando que 50% dos dados de nossa amostra são menores que 4.
Equivalências:
| Quantil | Quartil | Percentil |
|---|---|---|
| 0.25 | 1 | 25% |
| 0.5 | 2 | 50% |
| 0.75 | 3 | 75% |
A combinação de quantils com outras propriedades de uma amostra (como a média, valor mínimo, máximo e variância) nos fornece uma visão precisa da distribuição dos dados sob observação. Com o auxílio da biblioteca seaborn, podemos criar representação pictóricas destas informações, como no caso do Diagrama de Caixas (boxplot) e o Diagrama Violino (violinplot) apresentado abaixo:
import seaborn as sns
import matplotlib.pyplot as plt
# Ajustando o tamanho padrão das imagens e fontes
sns.set(font_scale=1.0, rc={
"figure.figsize": (10, 6),
})
ax = plt.subplot(121)
sns.boxplot(data=dados[["area"]], ax=ax)
plt.title("Box Plot"); plt.ylim([0, 300]);
ax = plt.subplot(122)
sns.violinplot(data=dados[["area"]], ax=ax)
plt.title("Violin Plot"); plt.ylim([0, 300]);
Podemos utilizar o argumento hue do seaborn para segmentar visualizações por categorias distintas com o emprego de cores.
fig, ax = plt.subplots()
sns.violinplot(
data=dados[dados.tipo.isin(["Tipo 1", "Tipo 2"])][["preco", "tipo"]],
y="preco",
x="tipo",
ax=ax
)
plt.title("Distribuição de preço por tipo de imóvel");
Além da distribuição das variáveis do nosso conjunto de dados (Análise Descritiva Univariada), podemos explorar relações entre pares utilizando a visualização pairplot do seaborn (Análise Descritiva Multivariada), que combina Funções de Densidade de Probabilidade (FDP, em inglês, Probability Density Function) com Gráficos de Disperção (scatterplot), como demonstrado na célula a seguir.
# A função .drop descarta algumas colunas do nosso DataFrame
sns.pairplot(
dados.drop(columns=["loc_x", "loc_y", "num_penit_4km", "num_penit_500m"])
)
<seaborn.axisgrid.PairGrid at 0x1b7e1126b10>
Utilizando a função corr do Pandas, junto com a visualização de mapa de calor do heatmap do seaborn, é possível criar um correlograma para mensurar, visualmente, as correlações entre as variáveis do conjunto de dados. Um adendo: a função corr admite apenas valores numéricos. Para contornar esse problema, podemos usar a função select_dtypes com o argumento exclude="object" para selecionar todas as colunas em que o tipo é diferente de object, ou seja, apenas as colunas que contém valores numéricos.
corr = dados.select_dtypes(exclude="object").corr().round(2)
fig, ax = plt.subplots(figsize=(12, 12))
sns.heatmap(corr,
annot=True,
square=True,
vmax=.8,
vmin=-.8,
linewidths=2,
cbar_kws={"shrink": .9})
<Axes: >
O conjunto de arquivos de dados imobiliários contém erros sistemáticos, dados faltantes e outliers.
Atividades:
Informações adicionais:
O prazo total para entrega do exercício é de 8 dias corridos, iniciando contagem a partir da data de disponibilização do exercício no Moodle;
A entrega deve ser feita no Moodle. Contudo, o exercício pode ser entregue em um arquivo comprimido OU via link compartilhado de um fork do projeto no github (ou alguma outra ferramenta de versionamento de código). Fica a critério do aluno escolher a melhor forma de entrega;
O arquivo (ou repositório git) da entrega, deve conter, obrigatóriamente:
exercicio.ipynb) com a resolução das atividades;dados_tratados.parquet, resultando do tratamento final da base de dados;Importando algumas bibliotecas
import seaborn as sns
import numpy as np
dados = dados[~dados[["tipo","cod_localidade"]].isna().any(axis=1)]
Padronizado a notação de escrita e mantido apenas as variáveis Tipo 1 e 2 pois eram as que mais tinham dados
dados.loc[:,"tipo"] = dados.tipo.str.capitalize()
dados = dados.query("tipo.isin(['Tipo 1','Tipo 2'])")
dados.tipo.value_counts()
tipo Tipo 2 981 Tipo 1 875 Name: count, dtype: int64
Padronizado a notação de localidade e mantido apenas as Localidades 1,2,3 e 4
dados.loc[:,"cod_localidade"] = dados.cod_localidade.str.capitalize()
dados = dados[dados.cod_localidade.apply(lambda x: False if (isinstance(x,str) and x.startswith("Tipo")) else True)]
dados = dados.query("cod_localidade.isin(['Localidade 1','Localidade 2','Localidade 3','Localidade 4'])")
dados.cod_localidade.value_counts()
cod_localidade Localidade 4 462 Localidade 3 457 Localidade 2 457 Localidade 1 443 Name: count, dtype: int64
Importando os dados do IBGE
regiao = pd.read_csv("estados.csv")
regiao.head()
| cod_uf_ibge | estado | uf | regiao | qtd_municipios | |
|---|---|---|---|---|---|
| 0 | 41 | PARANA | PR | REGIAO SUL | 399 |
| 1 | 42 | SANTA CATARINA | SC | REGIAO SUL | 295 |
| 2 | 43 | RIO GRANDE DO SUL | RS | REGIAO SUL | 497 |
| 3 | 15 | PARA | PA | REGIAO NORTE | 144 |
| 4 | 13 | AMAZONAS | AM | REGIAO NORTE | 62 |
Verificando se existe algum estado nos dados que não estão na importação do IBGE. Verifica-se a presença alguns campos Nulos na coluna uf, no mais parece estar tudo certo
dados[~dados.uf.isin(regiao.uf)].uf.describe()
count 0 unique 0 top NaN freq NaN Name: uf, dtype: object
Unindo os dados
dados = pd.merge(dados,regiao, left_on="uf",right_on="uf")
Verificando a quantidade de dados Nulos
print("feat_1: ",dados.feat_1.isna().sum())
print("feat_2: ",dados.feat_2.isna().sum())
print("feat_3: ",dados.feat_3.isna().sum())
print("feat_4: ",dados.feat_4.isna().sum(),"\n")
print("feat_1 e feat_2: ",sum(dados.feat_1.isna() & dados.feat_2.isna()))
print("feat_3 e feat_4: ",sum(dados.feat_3.isna() & dados.feat_4.isna()))
dados = dados.drop(dados[dados.feat_3.isna() & dados.feat_4.isna()].index,axis=0)
feat_1: 5 feat_2: 4 feat_3: 11 feat_4: 4 feat_1 e feat_2: 0 feat_3 e feat_4: 1
Realizando a regressão linear e o preenchimentos dos dados Nulos para as variáveis feat_1 e feat_2
dados_regressao_01 = dados[~dados[["feat_1","feat_2"]].isna().any(axis=1)].copy()
y_01 = dados_regressao_01.feat_1.values
x_01 = dados_regressao_01.feat_2.values
constante_01 = np.ones(len(y_01))
matriz_01 = np.vstack([constante_01,x_01]).T
k1,c1 = np.linalg.pinv(matriz_01).dot(y_01)
dados.loc[dados.feat_1.isna(),"feat_1"] = k1 + c1*dados.feat_2
dados.loc[dados.feat_2.isna(),"feat_2"] = (dados.feat_1 - k1)/c1
Realizando a regressão linear e o preenchimentos dos dados Nulos para as variáveis feat_3 e feat_4
dados_regressao_02 = dados[~dados[["feat_3","feat_4"]].isna().any(axis=1)].copy()
y_02 = dados_regressao_02.feat_3.values
x_02 = dados_regressao_02.feat_4.values
constante_02 = np.ones(len(y_02))
matriz_02 = np.vstack([constante_02,x_02]).T
k2,c2 = np.linalg.pinv(matriz_02).dot(y_02)
dados.loc[dados.feat_3.isna(),"feat_3"] = k2 + c2*dados.feat_4
dados.loc[dados.feat_4.isna(),"feat_4"] = (dados.feat_3 - k2)/c2
Verificando como ficou a questão de dados nulos para todas as colunas
dados.isna().sum(axis=0)
uf 0 tipo 0 cod_localidade 0 feat_1 0 feat_2 0 feat_3 0 feat_4 0 loc_x 6 loc_y 2 mercado_mais_proximo 63 farmacia_mais_proxima 58 escola_mais_proxima 58 num_penit_4km 8 num_penit_500m 2 idade_imovel 5 area 48 preco 0 cod_uf_ibge 0 estado 0 regiao 0 qtd_municipios 0 dtype: int64
Plotando um gráfico de dispersão para as variáveis feat_1 e feat_2
sns.scatterplot(x = dados.feat_2,y = dados.feat_1)
<Axes: xlabel='feat_2', ylabel='feat_1'>
Montando a matriz de covariancia e obtendo as medias para analisar os outliers
matriz_covariancia_1 = np.cov(dados.feat_1,dados.feat_2)
media_feat_1 = np.mean(dados.feat_1)
media_feat_2 = np.mean(dados.feat_2)
Função copiada do arquivo adicional_outliers para ordenar os autovalores e autovetores
def eigsorted(cov):
"""
Encontra os auto-valores e auto-vetores de uma matriz de variância-covariância.
Os auto-valores e auto-vetores nos ajudam a normalizar amostras de distribuições
multivariadas.
:cov: Matriz de variância-covariância.
"""
vals, vecs = np.linalg.eigh(cov)
order = vals.argsort()[::-1]
return vals[order], vecs[:,order]
Obtendo os autovalores e autovetores e centralizando-os no ponto (0,0)
autovalores_1, autovetores_1 = eigsorted(matriz_covariancia_1)
feat_1_outliers = dados.feat_1.copy() - media_feat_1
feat_2_outliers = dados.feat_2.copy() - media_feat_2
Rotacionando os dados conforme sentido que melhor expressa a variância das variaveis
rotacao_1 = np.vstack([feat_1_outliers,feat_2_outliers]).T.dot(autovetores_1)
Normalizando os dados com relação a variância e utilizando a distancia euclidiana para identificar e remover os outliers
Z_rotacao_1 = rotacao_1 / np.sqrt(np.diag(np.cov(rotacao_1,rowvar=0)))
outliers_1 = np.sqrt((Z_rotacao_1 ** 2).sum(axis=1)) > 2.5
dados = dados.loc[~outliers_1, :]
Plotando as variaveis feat_1 e feat_2 após remoção dos outliers
sns.scatterplot(x = dados.feat_2,y = dados.feat_1)
<Axes: xlabel='feat_2', ylabel='feat_1'>
Plotando um gráfico de dispersão para as variáveis feat_3 e feat_4
sns.scatterplot(x = dados.feat_4,y = dados.feat_3)
<Axes: xlabel='feat_4', ylabel='feat_3'>
Montando a matriz de covariancia e obtendo as medias para analisar os outliers
matriz_covariancia_2 = np.cov(dados.feat_3,dados.feat_4)
media_feat_3 = np.mean(dados.feat_3)
media_feat_4 = np.mean(dados.feat_4)
Obtendo os autovalores e autovetores e centralizando-os no ponto (0,0)
autovalores_2, autovetores_2 = eigsorted(matriz_covariancia_2)
feat_3_outliers = dados.feat_3.copy() - media_feat_3
feat_4_outliers = dados.feat_4.copy() - media_feat_4
Rotacionando os dados conforme sentido que melhor expressa a variância das variaveis
rotacao_2 = np.vstack([feat_3_outliers,feat_4_outliers]).T.dot(autovetores_2)
Normalizando os dados com relação a variância e utilizando a distancia euclidiana para identificar e remover os outliers
Z_rotacao_2 = rotacao_2 / np.sqrt(np.diag(np.cov(rotacao_2,rowvar=0)))
outliers_2 = np.sqrt((Z_rotacao_2 ** 2).sum(axis=1)) > 2.5
dados = dados.loc[~outliers_2, :]
Plotando um gráfico de dispersão para as variáveis feat_3 e feat_4
sns.scatterplot(x = dados.feat_4,y = dados.feat_3)
<Axes: xlabel='feat_4', ylabel='feat_3'>
Veridicando se algumas outras variaveis númericas possuem outliers, para isso esta sendo utilizado um gráfico Boxplot
colunas = ['mercado_mais_proximo', 'farmacia_mais_proxima','escola_mais_proxima']
dados[colunas][~dados[colunas].isna().any(axis=1)].boxplot()
<Axes: >
Das três variáveis avalidadas acima, constatou-se que todas tinham outliers, por isso foi realizado uma fórmula para remoção de variáveis com Z score acima de 2.5
def rem_outliers(distancia, threshold=2.5):
media = np.mean(distancia)
desvio = np.std(distancia)
adiciona_na = []
for d in distancia:
if ((d - media) / desvio) > threshold:
adiciona_na.append(np.nan)
else:
adiciona_na.append(d)
return adiciona_na
Aqui esta sendo aplicada a formula acima para as colunas com as variáveis apontadas acima
dados[colunas] = dados[colunas].apply(rem_outliers,axis=0)
Após isso foi feito um novo gráfico boxplot, constatando poucos resultados agora como outliers
dados[colunas][~dados[colunas].isna().any(axis=1)].boxplot()
<Axes: >
Removendo os outro dados Nulos que ainda estavam presentes no dataset
dados = dados[~dados.isna().any(axis=1)]
dados.isna().sum(axis=0)
uf 0 tipo 0 cod_localidade 0 feat_1 0 feat_2 0 feat_3 0 feat_4 0 loc_x 0 loc_y 0 mercado_mais_proximo 0 farmacia_mais_proxima 0 escola_mais_proxima 0 num_penit_4km 0 num_penit_500m 0 idade_imovel 0 area 0 preco 0 cod_uf_ibge 0 estado 0 regiao 0 qtd_municipios 0 dtype: int64
Resetando os indices do dataset
dados = dados.reset_index(drop=True)
Verificando com quantos dados ficou o dataset após todas as transformações
dados.shape
(1392, 21)
Plotando um gráfico de preço por localidade, percebe-se que a localidade 3 parece possuir maior valor de imóveis
sns.boxplot(y = dados.preco, x= dados.cod_localidade)
<Axes: xlabel='cod_localidade', ylabel='preco'>
Já com relação ao gráfico que relaciona preço com tipo, percebe-se que imóveis do Tipo 2 apresentam maior variância quando comparados ao Tipo 1
sns.boxplot(y = dados.preco, x= dados.tipo)
<Axes: xlabel='tipo', ylabel='preco'>
Plotando gráficos de disperção para as variáveis mercado_mais_proximo, farmacia_mais_proxima e escola_mais_proxima, parece que as informações não parecem estar correlacionadas
fig, axs = plt.subplots(1, 3,figsize=(20,5))
sns.scatterplot(x=dados.mercado_mais_proximo, y=dados.farmacia_mais_proxima, ax=axs[0])
sns.scatterplot(x=dados.mercado_mais_proximo, y=dados.escola_mais_proxima, ax=axs[1])
sns.scatterplot(x=dados.farmacia_mais_proxima, y=dados.escola_mais_proxima, ax=axs[2])
<Axes: xlabel='farmacia_mais_proxima', ylabel='escola_mais_proxima'>
Com relação as variáveis preço e área, não apresentam padrão claro entre elas
sns.scatterplot(x=dados.area, y=dados.preco)
<Axes: xlabel='area', ylabel='preco'>
Por fim salvando o dataset dados no formato parquet
dados.to_parquet("dados_tratados.parquet")